#include "jlog.h"

#undef jDebug
#undef jInfo
#undef jWarning
#undef jError
#undef jFatal

#include "private/jlog_p.h"

#include <QCoreApplication>
#include <QSharedPointer>
#include <QWaitCondition>
#include <QReadWriteLock>
#include <QDateTime>
#include <QThread>
#include <QEvent>
#include <QDir>
#include <cstdio>

#include <functional>
#include <iostream>

namespace QTE
{

struct Q_DECL_HIDDEN CurrLogFile
{
	QString name;
	QDate date;
	QTime last;

	void saveLastName()
	{
		auto oldName = name;
		name.chop(5);
		name += last.toString("hh:mm:ss:zzz");
		QFile::rename(oldName, name);
	}
};

typedef JMessageLogger::Info  MSGLOGInfo;

struct Q_DECL_HIDDEN LogInfo
{
public:
	LogInfo(const MSGLOGInfo &info, bool headerBreaksALine, QtMsgType type,
			const QMessageLogContext &context, const QString &msg);

public:
	MSGLOGInfo JMLInfo;
	bool headerBreaksALine;

	QtMsgType type;
	QDateTime time;
	QString msg;

	QString file;
	int line;
};

class Q_DECL_HIDDEN LogOutput : public QThread
{
public:
	~LogOutput();

public:
	void output(QtMsgType type, const QMessageLogContext &context, const QString &msg);
	void exit(bool block = true);

protected:
	void run() override;

private:
	void process(const QSharedPointer<LogInfo> &info) noexcept;

private:
	std::atomic_bool m_exitFlag {false};
	std::list<LogInfo*> m_logQueue;
	QWaitCondition m_condition;
	QMutex m_mutex;
};

/*---------------------------------------------------------------------------------------------------------*/

typedef JMessageLogger::FatalOperation  FatalOperation;

static class Q_DECL_HIDDEN JMessageLoggerImpl
{
public:
	explicit JMessageLoggerImpl(const MSGLOGInfo &info, FatalOperation ope);
	void infoSetting(const JMessageLogger::Info &info);

public:
	MSGLOGInfo m_info;
	QLoggingCategory *m_category = nullptr;
	LogOutput m_output;
	QReadWriteLock m_RWLock;

public:
	CurrLogFile m_currLogFile;
	std::atomic_bool m_headerBreaksALine {false};

public:
	FatalOperation m_fatalOpera = JMessageLogger::Abort;
	std::function<void(const QString&)> m_fatalOperaFunc {nullptr};
}
*gImpl = nullptr;

static JMessageLogger *gInstance = nullptr;

static QMutex gMutex;
static int gCounter = 0;

/*---------------------------------------------------------------------------------------------------------*/

JMessageLogger::JMessageLogger(const Info &info, FatalOperation ope)
{
	gMutex.lock();
	if( gImpl == nullptr )
		gImpl = new JMessageLoggerImpl(info, ope);

	if( gInstance == nullptr )
		gInstance = this;

	++gCounter;
	gMutex.unlock();
}

JMessageLogger::JMessageLogger
(const Info &info, const std::function<void(const QString&)> &customFatalOperation) :
	JMessageLogger(info, User)
{
	gImpl->m_fatalOperaFunc = customFatalOperation;
}

JMessageLogger::~JMessageLogger()
{
	gMutex.lock();
	if( --gCounter == 0 )
		delete gImpl;
	gMutex.unlock();
}

JMessageLogger *JMessageLogger::instance()
{
	return gInstance;
}

void JMessageLogger::setInfo(const JMessageLogger::Info &info)
{
	J_WRITE_LOCKER(gImpl->m_RWLock);
	gImpl->infoSetting(info);
}

static std::atomic_bool gFilterQTELOG {false};

void JMessageLogger::setFilterQTELOG(bool filter)
{
	gFilterQTELOG = filter;
}

void JMessageLogger::setHeaderBreaksALine(bool enable)
{
	gImpl->m_headerBreaksALine = enable;
}

static QtMessageHandler gLastMessageHandler = nullptr;

#ifdef Q_OS_WIN
# define _PS  "\\"
#else //other os
# define _PS  "/"
#endif //os

const QLoggingCategory &JMessageLogger::category() const
{
	gImpl->m_RWLock.lockForRead();
	if( gImpl->m_category )
	{
		gImpl->m_RWLock.unlock();
		return *gImpl->m_category;
	}
	gImpl->m_RWLock.unlock();

	const static QLoggingCategory category("");
	return category;
}

JMessageLogger::Info JMessageLogger::info() const
{
	J_READ_LOCKER(gImpl->m_RWLock);
	return gImpl->m_info;
}

JMessageLogger::FatalOperation JMessageLogger::fatalOperation() const
{
	return gImpl->m_fatalOpera;
}

bool JMessageLogger::headerBreaksALine() const
{
	return gImpl->m_headerBreaksALine;
}

void JMessageLogger::exit()
{
	gImpl->m_output.exit();
}

/*---------------------------------------------------------------------------------------------------------*/

inline LogInfo::LogInfo
(const MSGLOGInfo &info, bool headerBreaksALine, QtMsgType type,
 const QMessageLogContext &context, const QString &msg) :
	JMLInfo(info), headerBreaksALine(headerBreaksALine),
	type(type), time(QDateTime::currentDateTime()), msg(msg),
	file(context.file), line(context.line)
{
	JMLInfo.category = context.category;
}

LogOutput::~LogOutput()
{
	exit();
}

void LogOutput::output(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
	if( strcmp(context.category, privateCategoryName()) == 0 and gFilterQTELOG )
		return ;

	gImpl->m_RWLock.lockForRead();
	if( context.category != gImpl->m_info.category and gLastMessageHandler )
	{
		gImpl->m_RWLock.unlock();
		return gLastMessageHandler(type, context, msg);
	}

	auto info = new LogInfo(gImpl->m_info, gImpl->m_headerBreaksALine, type, context, msg);
	gImpl->m_RWLock.unlock();

	if( type == QtFatalMsg or type == QtCriticalMsg )
		return process(QSharedPointer<LogInfo>(info));

	else if( info->JMLInfo.async )
	{
		m_mutex.lock();
		m_logQueue.emplace_back(info);

		m_mutex.unlock();
		m_condition.wakeOne();
	}
	else
		process(QSharedPointer<LogInfo>(info));
}

void LogOutput::exit(bool block)
{
	m_exitFlag = true;
	if( not isRunning() )
		return ;

	m_condition.wakeOne();
	if( block )
		wait();
}

void LogOutput::run()
{
	for(;;)
	{
		J_QMUTEX_LOCKER(m_mutex);

		while( m_logQueue.empty() )
		{
			if( m_exitFlag )
				return ;

			m_condition.wait(&m_mutex);
		}

		std::list<LogInfo*> infoList(m_logQueue);
		m_logQueue.clear();

		J_MUTEX_LOCKER_UNLOCK();

		for(auto &info : infoList)
			process(QSharedPointer<LogInfo>(info));
	}
}

static bool openLogOutputDevice(const LogInfo *info, int logSize, FILE *stdDev, FILE **fp);

void LogOutput::process(const QSharedPointer<LogInfo> &info) noexcept
{
	QString logText;

	FILE *fp = nullptr;
	bool needClose = false;
	auto category = info->JMLInfo.category == "default"? "" : info->JMLInfo.category;

#define SET_LOG(level, type, stdfp) \
	if( info->JMLInfo.mask < level ) \
		return ; \
	logText = QString("[%1::" type "]-[%2]-[%3:%4]:%5").arg(category) \
				.arg(info->time.toString("yyyy-MM-dd hh:mm:ss:zzz")) \
				.arg(info->file).arg(info->line).arg(info->headerBreaksALine? "\n":" ") + info->msg; \
	needClose = openLogOutputDevice(info.data(), logText.toLocal8Bit().size(), stdfp, &fp)

	switch(info->type)
	{
		case QtDebugMsg   : SET_LOG(4, "Debug"  , info->JMLInfo.noStdout?stderr:stdout); break;
		case QtInfoMsg    : SET_LOG(3, "Info"   , info->JMLInfo.noStdout?stderr:stdout); break;
		case QtWarningMsg : SET_LOG(2, "Warning", stderr); break;
		case QtCriticalMsg: SET_LOG(1, "Error"  , stderr); break;
		case QtFatalMsg   : SET_LOG(0, "Fatal"  , stderr); break;

		default:
			std::cerr << "Error: JLog: process: Invalid message type" << std::endl;
			abort();
	}

	logText += "\n\n";
	auto buffer = logText.toLocal8Bit();

	int res = fwrite(buffer.data(), 1, buffer.size(), fp);
	if( res < buffer.size() and not info->JMLInfo.dir.isEmpty() )
		fprintf(stderr, "Error: JLog: process: The log file write error: %s.\n", strerror(errno));

	fflush(fp);
	if( needClose )
		fclose(fp);

	if( info->type == QtFatalMsg and gImpl->m_fatalOperaFunc )
		gImpl->m_fatalOperaFunc(info->msg);
}

// Log processing entry
static void messageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
	if( not msg.isEmpty() )
		gImpl->m_output.output(type, context, msg);
}

JMessageLoggerImpl::JMessageLoggerImpl(const MSGLOGInfo &info, FatalOperation ope) :
	m_fatalOpera(ope)
{
	infoSetting(info);

	if( not m_info.dir.isEmpty() )
		m_currLogFile.date = QDate::currentDate();

	if( ope != JMessageLogger::User )
	{
		if( ope == JMessageLogger::Abort )
			m_fatalOperaFunc = [](const QString&){ jLogger->exit(); abort(); };

		else if( gImpl->m_fatalOpera == JMessageLogger::Throw )
			m_fatalOperaFunc = [](const QString &msg){ jLogger->exit(); throw(msg.toStdString()); };

		else if( gImpl->m_fatalOpera == JMessageLogger::SysExit )
			m_fatalOperaFunc = [](const QString&){ jLogger->exit(); ::exit(-1); };

		else if( gImpl->m_fatalOpera == JMessageLogger::AppExit )
			m_fatalOperaFunc = [](const QString&){ jLogger->exit(); qApp->exit(-1); };

		else if( gImpl->m_fatalOpera == JMessageLogger::Continue )
			m_fatalOperaFunc = [](const QString&){};
	}

	gLastMessageHandler = qInstallMessageHandler(messageOutput);
}

void JMessageLoggerImpl::infoSetting(const JMessageLogger::Info &info)
{
	m_info = info;

	if( not m_info.dir.isEmpty() )
	{
		if( m_info.maxSizeOneFile < 1024 )
		{
			qCritical() << "Log max size one file settings too small. (>=1024)";
			m_info.maxSizeOneFile = 1024;
		}

		if( m_info.maxSizeOneDay < m_info.maxSizeOneFile )
		{
			qCritical() << "Log max size one day settings too small. (>=maxSizeOneFile)";
			m_info.maxSizeOneDay = m_info.maxSizeOneFile;
		}

		if( m_info.maxSize < m_info.maxSizeOneDay )
		{
			qCritical() << "Log max size settings too small. (>=maxSizeOneDay)";
			m_info.maxSize = m_info.maxSizeOneDay;
		}

#ifdef Q_OS_WIN
		m_info.dir.replace("/", "\\");
#endif //windowsNT

		if( not m_info.dir.endsWith(_PS) )
			m_info.dir += _PS;

		else if( not m_info.dir.startsWith(_PS) and qApp )
			m_info.dir = QCoreApplication::applicationDirPath() + _PS + m_info.dir;
	}

	auto tmp = m_info.category.toStdString();
	size_t len = tmp.length();
	if( len > 255 )
		len = 255;

	auto name = new char[256]();
	strncpy(name, tmp.c_str(), len);

	if( m_category )
		delete m_category;
	m_category = new QLoggingCategory(name);

	if( m_info.async )
		m_output.start();
	else
		m_output.exit(false);
}

/*---------------------------------------------------------------------------------------------------------*/

static void listPushBack(QFileInfoList &list, const QDir &dir)
{
	auto tmp = dir.entryInfoList(QDir::Files);

	std::sort(tmp.begin(), tmp.end(), [](const QFileInfo &info0, const QFileInfo &info1)
	{
#if QT_VERSION < QT_VERSION_CHECK(5,10,0)
		return info0.created() < info1.created();
#else
		return info0.birthTime() < info1.birthTime();
#endif
	});

	list << tmp;
}

static void removeIfTooBig(const QFileInfoList &list, qint64 maxSize,int logSize)
{
	qint64 size = 0;
	for(auto &info : list)
		size += info.size();

	if( size + logSize > maxSize )
	{
		for(auto &info : list)
		{
			QFile::remove(info.filePath());
			size -= info.size();

			if( size + logSize <= maxSize )
				break;
		}
	}
}

static void sizeCheck(const JMessageLogger::Info &info, const QString &dayPath, int logSize)
{
	QDir dir(dayPath);
	QFileInfoList fileInfoList;

	listPushBack(fileInfoList, dir);
	removeIfTooBig(fileInfoList, info.maxSizeOneDay, logSize);

	dir.setPath(info.dir);
	auto dirInfoList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);

	std::sort(dirInfoList.begin(), dirInfoList.end(), [](const QFileInfo &info0, const QFileInfo &info1)
	{
#if QT_VERSION < QT_VERSION_CHECK(5,10,0)
		return info0.created() < info1.created();
#else
		return info0.birthTime() < info1.birthTime();
#endif
	});

	fileInfoList.clear();
	for(auto &info : dirInfoList)
		listPushBack(fileInfoList, info.filePath());
	removeIfTooBig(fileInfoList, info.maxSize, logSize);

	if( dirInfoList.size() == 1 )
		return ;

	for(auto &info : dirInfoList)
	{
		dir.setPath(info.filePath());

#if QT_VERSION < QT_VERSION_CHECK(5,9,0)
		if( QDir(info.filePath()).entryList(QDir::Files).isEmpty() )
#else //Qt>=5.9.0
		if( QDir(info.filePath()).isEmpty(QDir::Files) )
#endif //5.9.0
			dir.removeRecursively();
		else
			break;
	}
}

static bool openLogOutputDevice(const LogInfo *info, int logSize, FILE *stdDev, FILE **fp)
{
	if( info->JMLInfo.dir.isEmpty() )
	{
		*fp = stdDev;
		return false;
	}

	if( info->time.date() > gImpl->m_currLogFile.date )
	{
		gImpl->m_currLogFile.saveLastName();
		gImpl->m_currLogFile.date = info->time.date();
		gImpl->m_currLogFile.name = "";
	}

	QDir dir;
	QString dirName = info->JMLInfo.dir + gImpl->m_currLogFile.date.toString("yyyy-MM-dd") + _PS;

	if( not info->JMLInfo.dirLv2.isEmpty() )
	{
		dirName += info->JMLInfo.dirLv2;
		if( not dirName.endsWith(_PS) )
			dirName += _PS;
	}

	if( not dir.exists(dirName) and dir.mkpath(dirName) == false )
	{
		std::cerr << "Error: JLog: openLogOutputDevice: Log directory make failed!" << std::endl;
		*fp = stdDev;
		return false;
	}

	if( gImpl->m_currLogFile.name.isEmpty() )
	{
		for(auto &fileName : QDir(dirName).entryList())
		{
			if( fileName.endsWith("(now)") )
				gImpl->m_currLogFile.name = dirName + fileName;
		}

		if( gImpl->m_currLogFile.name.isEmpty() )
			gImpl->m_currLogFile.name = dirName + info->time.time().toString("hh:mm:ss:zzz-(now)");
	}

	else if( QFileInfo(gImpl->m_currLogFile.name).size() + logSize > info->JMLInfo.maxSizeOneFile )
	{
		gImpl->m_currLogFile.saveLastName();
		gImpl->m_currLogFile.name = dirName + info->time.time().toString("hh:mm:ss:zzz-(now)");
	}

	sizeCheck(info->JMLInfo, dirName, logSize);
	gImpl->m_currLogFile.last = info->time.time();

	auto stdFileName = gImpl->m_currLogFile.name.toStdString();
	*fp = fopen(stdFileName.c_str(), "a");

	if( *fp == nullptr )
	{
		fprintf(stderr, "Error: JLog: openLogOutputDevice: The log file '%s' open failed: %s.\n", stdFileName.c_str(), strerror(errno));
		*fp = stdDev;
		return false;
	}

	return true;
}

} //namespace QTE
